#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
THN DATA ABSTRACTION LAYER version 1

Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

This library is dual-licensed.
  + For commercial software, please obtain a commercial license from THN Solutions LLC.

  + For free software, this library is licensed under GPL version 3. A summary of GPLv3 is
    listed below. You should also find a copy of GPLv3 as file License_GPLv3.txt included
	with the source-code files.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 3 as published by
    the Free Software Foundation  of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#endregion

#region Using directives
using System;
using System.Collections;
using Thn.Application;
using Thn.Application.Utilities;
using Thn.Data.Query;
using Thn.Data.Settings;
#endregion

namespace Thn.Data.Persistence
{
    /// <summary>
    /// A persistent layer provides a foundation for persitent contexts.
    /// </summary>
    public class PersistentLayer : IPersistentLayer
    {
        #region Events

        #region Before Execute Query
        /// <summary>
        /// This event is raised before executing a query
        /// </summary>
        public event QueryEventHandler BeforeExecuteQuery;

        /// <summary>
        /// Raise event BeforeExecuteQuery
        /// </summary>
        protected void RaiseBeforeExecuteQuery(QueryBase query)
        {
            QueryEventHandler handler = BeforeExecuteQuery;
            if (handler != null) handler(this, query);
        }
        #endregion

        #region After Execute Query
        /// <summary>
        /// This event is raised after executing a query
        /// </summary>
        public event QueryEventHandler AfterExecuteQuery;

        /// <summary>
        /// Raise event AfterExecuteQuery
        /// </summary>
        protected void RaiseAfterExecuteQuery(QueryBase query)
        {
            QueryEventHandler handler = AfterExecuteQuery;
            if (handler != null) handler(this, query);
        }
        #endregion

        #region Before Process Commands
        /// <summary>
        /// This event is raised before processing commands
        /// </summary>
        public event CommandEventHandler BeforeProcessCommands;

        /// <summary>
        /// Raise event BeforeProcessCommands
        /// </summary>
        protected void RaiseBeforeProcessCommands(CommandCollection commands)
        {
            CommandEventHandler handler = BeforeProcessCommands;
            if (handler != null) handler(this, commands);
        }
        #endregion

        #region After Process Commands
        /// <summary>
        /// This event is raised after processing commands
        /// </summary>
        public event CommandEventHandler AfterProcessCommands;

        /// <summary>
        /// Raise event AfterProcessCommands
        /// </summary>
        protected void RaiseAfterProcessCommands(CommandCollection commands)
        {
            CommandEventHandler handler = AfterProcessCommands;
            if (handler != null) handler(this, commands);
        }
        #endregion
	
        #endregion

        #region Database
        private IDatabase mDatabase;
        /// <summary>
        /// Gets/Sets the database associated with this persistence layer
        /// </summary>
        public IDatabase Database
        {
            get { return mDatabase; }
            set { mDatabase = value; }
        }
        #endregion

        #region Create Context
        /// <summary>
        /// Create a new persitent context associated with this layer
        /// </summary>
        public IPersistentContext CreateContext()
        {
            PersistentContext result = new PersistentContext(this);

            return result;
        }
        #endregion

        #region Interceptors
        Hashtable mInterceptors = null;

        #region Register Interceptor
        /// <summary>
        /// Subscribe an interceptor for processing persistent commands
        /// </summary>
        /// <param name="interceptor">The interceptor to register</param>
        /// <param name="objectType">The type of persistent object this interceptor is interested in</param>
        public void RegisterInterceptor(IPersistentInterceptor interceptor, Type objectType)
        {
            //create cache
            if (mInterceptors == null) mInterceptors = new Hashtable();

            //obtain collection of interceptors interested in same object type
            IList lst = null;
            if (mInterceptors.Contains(objectType)) lst = (IList)mInterceptors[objectType];
            else
            {
                //none available yet, create new list and add to cache
                lst = new ArrayList();
                mInterceptors.Add(objectType, lst);
            }

            //add interceptor
            lst.Add(interceptor);
        }
        #endregion

        #region Get Interceptors
        /// <summary>
        /// Gets a collection of interceptors interested in a particular type of object
        /// </summary>
        /// <param name="objectType">Type of persistent object</param>
        /// <returns>null if no interceptors interested</returns>
        protected IList GetInterceptors(Type objectType)
        {
            IList result = null;

            if (mInterceptors != null)
            {
                if (mInterceptors.Contains(objectType))
                {
                    result = (IList)mInterceptors[objectType];
                }
            }

            return result;
        }
        #endregion

        #endregion

        #region Process Commands
        #region Process Commands
        /// <summary>
        /// Process a collection of persistent commands to persist changes to database
        /// </summary>
        /// <returns>True when everything goes well, false if a command fails</returns>
        public bool ProcessCommands(CommandCollection commands)
        {
            if (BeforeProcessCommands != null) RaiseBeforeProcessCommands(commands);
            
            bool result = OnProcessCommands(commands);

            if (AfterProcessCommands != null) RaiseAfterProcessCommands(commands);

            return result;
        }
        #endregion

        #region On Process Commands
        /// <summary>
        /// Process a collection of persistent commands to persist changes to database
        /// </summary>
        /// <returns>True when everything goes well, false if a command fails</returns>
        protected virtual bool OnProcessCommands(CommandCollection commands)
        {
            bool success = true;

            success = ProcessBatch(commands, null) == 0;

            return success;
        }
        #endregion

        #region Simplify Batch
        /// <summary>
        /// Reduces duplicate & overlapping commands before processing for database.
        /// <para>Rules:</para>
        /// <para>   + if an object is newly added then removed, all commands involving that object is discarded</para>
        /// <para>   + if an object is added then updated multiple times, only the add command is processed</para>
        /// <para>   + if an object is updated multiple times, only one update command is processed</para>
        /// </summary>
        protected CommandCollection SimplifyBatch(CommandCollection commands)
        {
            CommandCollection result = new CommandCollection();            
            Hashtable processedCommands = new Hashtable();
            object id = null;

            for (int i = 0; i < commands.Count; i++)
            {
                PersistentCommand cmd = commands[i];

                if ((cmd.Status == PersistentCommandStatus.NotProcessed) || (cmd.Status == PersistentCommandStatus.Failed))
                {
                    if (cmd is PersistentObjectCommand)
                    {
                        PersistentObjectCommand poc = (PersistentObjectCommand)cmd;
                        IPersistentObject obj = poc.Target;
                        id = obj.GetIdentifier();

                        if (processedCommands.Contains(id))
                        {//already processed before
                            PersistentCommand prev = (PersistentCommand)processedCommands[id];
                            if (cmd is AddObjectCommand)
                            {
                                //current command is ADD, previous is REMOVE => proceed normally
                                if (prev is RemoveObjectCommand)
                                {
                                    result.Add(cmd);
                                    processedCommands.Remove(id);
                                    processedCommands.Add(id, cmd);
                                }
                                else cmd.Status = PersistentCommandStatus.Skipped;
                            }
                            else if (cmd is RemoveObjectCommand)
                            {
                                //current is REMOVE, previous is ADD or UPDATE => discard previous
                                if ((prev is AddObjectCommand) || (prev is UpdatePropertyCommand) || (prev is UpdateObjectCommand))
                                {
                                    result.Remove(prev);
                                    processedCommands.Remove(id);
                                }
                                else cmd.Status = PersistentCommandStatus.Skipped;
                            }
                            else cmd.Status = PersistentCommandStatus.Skipped;
                        }
                        else
                        {//not yet processed
                            result.Add(cmd);
                            processedCommands.Add(id, cmd);
                        }
                    }
                    else result.Add(cmd);

                }//cmd.Status = NotProcessed or Failed
            }

            processedCommands.Clear();

            return result;
        }
        #endregion

        #region Process Batch
        /// <summary>
        /// Process a collection of commands
        /// </summary>
        /// <param name="commands">Collection of commands to process</param>
        /// <param name="query">[optional] transaction that the new execution should be contained within</param>
        /// <returns>the number of failed commands</returns>
        protected virtual int ProcessBatch(CommandCollection commands, TransactionQuery query)
        {
            int errorCount = 0;

            commands = SimplifyBatch(commands);

            for (int i = 0; i < commands.Count; i++)
            {
                PersistentCommand cmd = commands[i];

                if (cmd is PersistentObjectCommand)
                {
                    PersistentObjectCommand poc = (PersistentObjectCommand)cmd;
                    IPersistentObject obj = poc.Target;

                    if (obj.Persistent)
                    {//this object has been persisted to database before
                        if ((cmd is AddObjectCommand) || (cmd is UpdatePropertyCommand) || (cmd is UpdateObjectCommand)) ProcessUpdate(poc, query);
                        else if (cmd is RemoveObjectCommand) ProcessRemove(poc, query);
                    }
                    else
                    {//this object has not been persisted before
                        if (cmd is AddObjectCommand) ProcessAdd(poc, query);
                        else if ((cmd is UpdatePropertyCommand) || (cmd is UpdateObjectCommand)) ProcessUpdate(poc, query);
                    }

                    if (poc.Status == PersistentCommandStatus.Failed) errorCount++;
                }
                else if (cmd is TransactionCommand)
                {
                    TransactionCommand txc = (TransactionCommand)cmd;
                    if (txc.Commands.Count > 0)
                    {
                        TransactionQuery transaction = new TransactionQuery();
                        ProcessBatch(txc.Commands, transaction);

                        if (transaction.Transaction.Commands.Count > 0)
                        {
                            if (query != null) query.Transaction.Commands.Add(transaction);
                            else
                            {
                                object logScope = null;
                                bool writeLog = DataTierSettings.LogCommand;

                                if (writeLog)
                                {                                    
                                    logScope = Log.Start("[" + this.GetType().Name + "] Transaction");
                                }

                                try
                                {
                                    mDatabase.Execute(transaction);
                                    txc.Status = PersistentCommandStatus.Succeeded;
                                }
                                catch (Exception ex)
                                {
                                    txc.Status = PersistentCommandStatus.Failed;
                                    txc.Error = ex;
                                    errorCount++;

                                    //process error
                                    if (DataTierSettings.AutoPublishError) Log.Error(ex);
                                    if (!DataTierSettings.SilentError) throw ex;
                                }

                                if (writeLog)
                                {
                                    Log.End(logScope);
                                }
                            }
                        }
                    }
                }
            }

            return errorCount;
        }
        #endregion

        #region Process Add
        /// <summary>
        /// Execute an insert command
        /// </summary>
        /// <param name="command">The command to process</param>
        /// <param name="query">[optional] transaction that the new execution should be contained within</param>
        protected virtual void ProcessAdd(PersistentObjectCommand command, TransactionQuery query)
        {
            IPersistentObject target = command.Target;
            object logScope = null;
            bool writeLog = DataTierSettings.LogCommand;

            if (writeLog)
            {
                logScope = Log.Start("[" + this.GetType().Name + "] ADD");
            }

            //set time stamps
            if (target is ITimeStampedObject)
            {
                ITimeStampedObject tso = (ITimeStampedObject)target;
                DateTime now = Clock.Now;
                tso.SetCreatedOn(now);
                tso.SetModifiedOn(now);
            }

            //invoke interceptors
            IList interceptors = GetInterceptors(target.GetType());
            TransactionQuery subQuery = null; 
            if (interceptors != null)
            {
                subQuery = new TransactionQuery();
                subQuery.Transaction.Commands.Add(new InsertQuery(target));

                //iterate through all interceptors and ask them to process this command
                for (int i = 0; i < interceptors.Count; i++)
                {
                    IPersistentInterceptor interceptor = (IPersistentInterceptor)interceptors[i];
                    interceptor.ProcessAdd(target, subQuery);
                }
            }

            //process command
            if (query == null)
            {
                try
                {                    
                    if ((subQuery == null) || (subQuery.Transaction.Commands.Count <= 1))
                    {//interceptors didn't bother to add any new commands, execute directly                                           
                        mDatabase.Insert(target).Execute();
                    }
                    else
                    {
                        mDatabase.Execute(subQuery);
                    }

                    //post-process to update status flags
                    command.Status = PersistentCommandStatus.Succeeded;
                    if (target is PersistentObject)
                    {
                        PersistentObject po = (PersistentObject)target;
                        po.SetPersistentFlag(true);
                        po.SetModifiedFlag(false);
                    }
                }
                catch (Exception ex)
                {
                    command.Status = PersistentCommandStatus.Failed;
                    command.Error = ex;

                    //process error
                    if (DataTierSettings.AutoPublishError) Log.Error(ex);
                    if (!DataTierSettings.SilentError) throw ex;
                }
            }//query == null
            else
            {//query != null
                if ((subQuery == null) || (subQuery.Transaction.Commands.Count <= 1))
                {//interceptors didn't bother to add any new commands, execute directly                   
                    query.Transaction.Commands.Add(new InsertQuery(target));
                }
                else
                {                    
                    query.Transaction.Commands.Add(subQuery);
                }
            }//query != null

            if (writeLog)
            {
                Log.End(logScope);
            }
        }
        #endregion

        #region Process Remove
        /// <summary>
        /// Execute a delete command
        /// </summary>
        /// <param name="command">The command to process</param>
        /// <param name="query">[optional] transaction that the new execution should be contained within</param>
        protected virtual void ProcessRemove(PersistentObjectCommand command, TransactionQuery query)
        {
            IPersistentObject target = command.Target;
            object logScope = null;
            bool writeLog = DataTierSettings.LogCommand;

            if (writeLog)
            {
                logScope = Log.Start("[" + this.GetType().Name + "] REMOVE");
            }

            //invoke interceptors
            IList interceptors = GetInterceptors(target.GetType());
            TransactionQuery subQuery = null;
            if (interceptors != null)
            {
                subQuery = new TransactionQuery();
                subQuery.Transaction.Commands.Add(new DeleteQuery(target));

                //iterate through all interceptors and ask them to process this command
                for (int i = 0; i < interceptors.Count; i++)
                {
                    IPersistentInterceptor interceptor = (IPersistentInterceptor)interceptors[i];
                    interceptor.ProcessAdd(target, subQuery);
                }
            }

            //process command
            if (query == null)
            {
                try
                {                    
                    if ((subQuery == null) || (subQuery.Transaction.Commands.Count <= 1))
                    {//interceptors didn't bother to add any new commands, execute directly                                           
                        mDatabase.Delete(target).Execute();
                    }
                    else
                    {
                        mDatabase.Execute(subQuery);
                    }

                    //post-process to update status flags
                    command.Status = PersistentCommandStatus.Succeeded;
                    if (target is PersistentObject)
                    {
                        PersistentObject po = (PersistentObject)target;
                        po.SetPersistentFlag(true);
                        po.SetModifiedFlag(false);
                    }
                }
                catch (Exception ex)
                {
                    command.Status = PersistentCommandStatus.Failed;
                    command.Error = ex;

                    //process error
                    if (DataTierSettings.AutoPublishError) Log.Error(ex);
                    if (!DataTierSettings.SilentError) throw ex;
                }
            }//query == null
            else
            {//query != null
                if ((subQuery == null) || (subQuery.Transaction.Commands.Count <= 1))
                {//interceptors didn't bother to add any new commands, execute directly                   
                    query.Transaction.Commands.Add(new DeleteQuery(target));
                }
                else
                {
                    query.Transaction.Commands.Add(subQuery);
                }
            }//query != null

            if (writeLog)
            {
                Log.End(logScope);
            }
        }
        #endregion

        #region Process Update
        /// <summary>
        /// Execute an update command
        /// </summary>
        /// <param name="command">The command to process</param>
        /// <param name="query">[optional] transaction that the new execution should be contained within</param>
        protected virtual void ProcessUpdate(PersistentObjectCommand command, TransactionQuery query)
        {
            IPersistentObject target = command.Target;
            object logScope = null;
            bool writeLog = DataTierSettings.LogCommand;
            DateTime oldModifiedOn = Nulls.DateTime;

            if (writeLog)
            {
                logScope = Log.Start("[" + this.GetType().Name + "] UPDATE");
            }

            //set time stamps
            if (target is ITimeStampedObject)
            {
                ITimeStampedObject tso = (ITimeStampedObject)target;
                DateTime now = Clock.Now;
                oldModifiedOn = tso.ModifiedOn;
                tso.SetModifiedOn(now);
            }

            //invoke interceptors
            IList interceptors = GetInterceptors(target.GetType());
            TransactionQuery subQuery = null;
            if (interceptors != null)
            {
                subQuery = new TransactionQuery();
                subQuery.Transaction.Commands.Add(new UpdateQuery(target));

                //iterate through all interceptors and ask them to process this command
                for (int i = 0; i < interceptors.Count; i++)
                {
                    IPersistentInterceptor interceptor = (IPersistentInterceptor)interceptors[i];
                    interceptor.ProcessAdd(target, subQuery);
                }
            }

            //process command
            if (query == null)
            {
                try
                {
                    if ((subQuery == null) || (subQuery.Transaction.Commands.Count <= 1))
                    {//interceptors didn't bother to add any new commands, execute directly                                           
                        mDatabase.Update(target).Execute();
                    }
                    else
                    {
                        mDatabase.Execute(subQuery);
                    }

                    //post-process to update status flags
                    command.Status = PersistentCommandStatus.Succeeded;
                    if (command.Target is PersistentObject)
                    {
                        PersistentObject po = (PersistentObject)target;
                        po.SetPersistentFlag(true);
                        po.SetModifiedFlag(false);
                    }
                }
                catch (Exception ex)
                {
                    command.Status = PersistentCommandStatus.Failed;
                    command.Error = ex;

                    //reverse time stamps
                    if (command.Target is ITimeStampedObject)
                    {
                        ITimeStampedObject tso = (ITimeStampedObject)target;
                        tso.SetModifiedOn(oldModifiedOn);
                    }

                    //process error
                    if (DataTierSettings.AutoPublishError) Log.Error(ex);
                    if (!DataTierSettings.SilentError) throw ex;
                }
            }//query == null
            else
            {//query != null
                if ((subQuery == null) || (subQuery.Transaction.Commands.Count <= 1))
                {//interceptors didn't bother to add any new commands, execute directly                   
                    query.Transaction.Commands.Add(new UpdateQuery(target));
                }
                else
                {
                    query.Transaction.Commands.Add(subQuery);
                }
            }//query != null

            if (writeLog)
            {
                Log.End(logScope);
            }
        }
        #endregion
        #endregion

        #region IQueryProcessor Members
        #region Execute
        /// <summary>
        /// Execute query
        /// </summary>
        ExecuteResult IQueryProcessor.Execute(QueryBase command)
        {
            if (BeforeExecuteQuery != null) RaiseBeforeExecuteQuery(command);

            ExecuteResult result = OnExecuteQuery(command);

            if (AfterExecuteQuery != null) RaiseAfterExecuteQuery(command);

            return result;
        }
        #endregion

        #region On Execute Query
        /// <summary>
        /// Execute query
        /// </summary>
        protected virtual ExecuteResult OnExecuteQuery(QueryBase command)
        {
            ExecuteResult result = null;

            if (mDatabase != null) result = mDatabase.Execute(command);
            else Thn.Exceptions.NullOrEmptyException.Publish("Database");

            return result;
        }
        #endregion
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public PersistentLayer()
        { }
        #endregion
    }

    /// <summary>
    /// Handler for events involving a query
    /// </summary>
    /// <param name="sender">The object that raised this event</param>
    /// <param name="query">The involved query</param>
    public delegate void QueryEventHandler(object sender, QueryBase query);

    /// <summary>
    /// Handler for events involving a command
    /// </summary>
    /// <param name="sender">The object that raised this event</param>
    /// <param name="commands">The involved command collection</param>
    public delegate void CommandEventHandler(object sender, CommandCollection commands);
}
